/*
 * @Author: soso
 * @Date: 2022-02-16 15:05:31
 * @LastEditTime: 2022-03-14 16:30:33
 * @LastEditors: Please set LastEditors
 * @Description: 探测兄弟节点，只支持ipv4
 * @FilePath: /gomeshsync/pingpong/pingpong.go
 */
package pingpong

import (
	"bytes"
	"fmt"
	"gomeshsync/consts"
	"io/ioutil"
	"net"
	"os"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"gitee.com/kimsoso/go-utils/sonet"
	"gitee.com/kimsoso/go-utils/utils"
	"github.com/vmihailenco/msgpack"
	"gopkg.in/yaml.v3"
)

var (
	oncePP   sync.Once
	singlePP *Pingpong
)

// node活跃节点探测
type Pingpong struct {
	path          string
	Nodes         []string `yaml:"nds"`
	checkingNodes atomic.Value
	nodes         atomic.Value
}

// 创建探测对象
func NewPingpong(nodesfilepath string) *Pingpong {
	oncePP.Do(func() {
		singlePP = &Pingpong{
			path:  nodesfilepath,
			Nodes: []string{},
		}
		singlePP.load()
		singlePP.clearRunning()
	})
	return singlePP
}

// 得到正在探测中的数据
func (pp *Pingpong) GetValidIps() (out []string) {
	ips := pp.checkingNodes.Load().(map[string]bool)
	out = []string{}
	for ip, ok := range ips {
		if ok {
			out = append(out, ip)
		}
	}
	return
}

// 在探测ip中加入一条
func (pp *Pingpong) addRunningIp(ip string) {
	ips := pp.checkingNodes.Load().(map[string]bool)
	ips[ip] = true
	pp.checkingNodes.Store(ips)
}

// 得到全网机器列表
func (pp *Pingpong) GetNodes() []string {
	return pp.nodes.Load().([]string)
}

// 探测局域网内其他机器
func (pp *Pingpong) Ping(port int) {
	netMask := getNetseg()

	// 探测局域网内所有ip
	for i := 1; i < 255; i++ {
		ip := fmt.Sprintf("%s.%d", netMask, i)

		if !pp.checkingNodes.Load().(map[string]bool)[ip] {
			newIps, ok := pp.ping(ip, port)

			if ok {
				ips := pp.checkingNodes.Load().(map[string]bool)
				ips[ip] = true

				for _, nip := range newIps {
					ips[nip] = true
				}
				pp.checkingNodes.Store(ips)
			}
		}
	}

	// 存储得到的ip列表
	ips := pp.checkingNodes.Load().(map[string]bool)
	newIps := []string{}
	for ip, ok := range ips {
		if ok {
			newIps = append(newIps, ip)
		}
	}
	pp.nodes.Store(newIps)
	pp.saveToDisk()

	// 清理探测临时变量
	pp.clearRunning()
}

func (pp *Pingpong) saveToDisk() {
	data, e := yaml.Marshal(pp.GetNodes())
	if e != nil {
		return
	}
	ioutil.WriteFile(pp.path, data, os.ModePerm)
}

// reply for ping
func (pp *Pingpong) Pong(conn net.Conn, cmd []byte) {
	params := bytes.Split(cmd, []byte(":"))
	ip := string(params[2])

	ips := pp.GetValidIps()

	reply := []byte(consts.COMM_PONG + ":")
	if len(ips) > 0 {
		data, _ := msgpack.Marshal(ips)
		reply = append(reply, data...)
	}

	pp.addRunningIp(ip)
	sonet.Write(conn, reply)
}

// 初始化节点临时存储
func (pp *Pingpong) clearRunning() {
	netmask := getNetseg()
	ips := map[string]bool{}
	for i := 1; i < 255; i++ {
		ip := fmt.Sprintf("%s.%d", netmask, i)
		ips[ip] = false
	}
	pp.checkingNodes.Store(ips)
}

func (pp *Pingpong) ping(ip string, port int) (out []string, success bool) {
	out = []string{}
	conn, e := timeoutConn(ip, port)
	if e != nil {
		return nil, false
	}
	defer conn.Close()

	e = sonet.Write(conn, []byte(consts.COMM_PING+":"+utils.GetLocalIPOnce()))
	if e != nil {
		return nil, false
	}

	_, d, e := sonet.Read(conn)
	if e != nil {
		return nil, false
	}

	if bytes.HasPrefix(d, []byte(consts.COMM_PONG)) {
		data := bytes.Split(d, []byte(":"))[2]
		if len(data) > 0 {
			yaml.Unmarshal(data, &out)
		}
		return out, true
	}

	return nil, false
}

// 加载本地保存
func (pp *Pingpong) load() {
	pp.nodes.Store(pp.Nodes)

	d, e := ioutil.ReadFile(pp.path)
	if e != nil {
		return
	}

	e = yaml.Unmarshal(d, &pp.Nodes)
	if e == nil {
		pp.nodes.Store(pp.Nodes)
	}
}

// 局域网段
func getNetseg() string {
	ips := strings.Split(utils.GetLocalIPOnce(), ".")
	if len(ips) != 4 {
		panic("got local ip mask error")
	}
	ips = ips[:3]
	return strings.Join(ips, ".")
}

// 带超时的连接, 500ms
func timeoutConn(ip string, port int) (conn net.Conn, err error) {
	dialer := net.Dialer{Timeout: time.Millisecond * 500}
	return dialer.Dial("tcp", fmt.Sprintf("%s:%d", ip, port))
}
